package elsim.scenarios

import java.util.Iterator;

import elsim.common.ElSimConstants;
import elsim.controller.Controller

import org.apache.log4j.Logger;
import org.joda.time.DateTime
import org.joda.time.DateTimeConstants;
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.DateTimeFormatter

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;

/**
 * Reads emulation scenario and calls passed controller. It is important
 * to understand, that in real-world application controller (which receives
 * and dispatches events into queue) and every lift will work in async
 * manner. Here it is not a requirement, so instead of building a separate
 * thread or process with scenario reader, we will build the event queue
 * before hand. However, the way most of the classes are written allows
 * change this easily into fully-async model.
 * 
 * @author kirillov
 */
class ScenarioReader {
  @VisibleForTesting enum EventType {
    Call,
    Fail,
    Fix,
    Stats,
    Display
  }

  private static final DateTimeFormatter format = DateTimeFormat.forPattern("HH:mm:ss");
  private static final Logger LOG = Logger.getLogger("ScenarioReader")

  private final Iterable<String> scenario;
  private final Controller controller;
  private int stepCounter = 0;

  private DateTime startTime = null;
  private int lifts = 0
  private int floors = 0

  public ScenarioReader(Iterable<String> scenario, Controller controller) {
    this.scenario = scenario;
    this.controller = controller;
  }

  static DateTime parseDateTime(String dateTime) {
    return format.parseDateTime(dateTime)
  }

  private void numberOfFloors(String line) {
    assert(line.startsWith("NumberOfFloors"));
    String startTimeVal = line.split("\t")[1];
    startTime = format.parseDateTime(startTimeVal);
  }

  public void readScenario() {
    Iterator<String> iterator = scenario.iterator();

    // there must be at least three lines there
    initialize(iterator);
    initLifts(iterator);

    while (iterator.hasNext()) {
      String line = iterator.next();
      handleLine(line)
    }
  }

  String[] splitOnSeparator(String line) {
    return line.split("\t")
  }

  @VisibleForTesting int dateTimeToTicks(DateTime dateTime) {
    Preconditions.checkNotNull(
        startTime,
        "startTime must be initialized before calling this method")

    Preconditions.checkArgument(
        dateTime.getMillis() >= startTime.getMillis(),
        "dateTime must be after startTime")

    return (dateTime.getMillis() - startTime.getMillis()) / DateTimeConstants.MILLIS_PER_SECOND
  }

  @VisibleForTesting void initialize(Iterator iterator) {
    String startTimeLine = iterator.next()
    String numberOfFloorsLine = iterator.next()
    String numberOfElevators = iterator.next()

    startTime = parseDateTime(splitOnSeparator(startTimeLine)[1])
    lifts = Integer.parseInt(splitOnSeparator(numberOfElevators)[1])
    floors = Integer.parseInt(splitOnSeparator(numberOfFloorsLine)[1])

    controller.onGlobalInit(lifts, floors, startTime)
  }

  @VisibleForTesting void initLifts(Iterator iterator) {
    for (int i = 0; i < lifts; ++i) {
      String liftInitLine = iterator.next();
      assert(liftInitLine.startsWith("init"));
      String[] fields = splitOnSeparator(liftInitLine)

      controller.onInitLift(
          Integer.parseInt(fields[1]),
          Integer.parseInt(fields[2]) * ElSimConstants.SECONDS_PER_FLOOR)
    }
  }

  @VisibleForTesting void handleCall(String line) {
    String[] fields = splitOnSeparator(line)
    String name = fields[1]
    int from = Integer.parseInt(fields[2])
    int clock = dateTimeToTicks(parseDateTime(fields[3]))
    int to = Integer.parseInt(fields[4])

    controller.onCall(name, from * ElSimConstants.SECONDS_PER_FLOOR, to * ElSimConstants.SECONDS_PER_FLOOR, clock)
  }

  @VisibleForTesting void handleFailOrFix(String line, EventType type) {
    String[] fields = splitOnSeparator(line)

    if (EventType.Fail.equals(type)) {
      controller.onFail(
          Integer.parseInt(fields[1]),
          dateTimeToTicks(parseDateTime(fields[2])))
    } else {
      controller.onFix(
          Integer.parseInt(fields[1]),
          dateTimeToTicks(parseDateTime(fields[2])))
    }
  }

  @VisibleForTesting void handleDisplayOrStats(String line, EventType type) {
    String[] fields = splitOnSeparator(line)

    if (EventType.Stats.equals(type)) {
      controller.onStats(
          dateTimeToTicks(parseDateTime(fields[1])))
    } else {
      controller.onDisplay(
          dateTimeToTicks(parseDateTime(fields[1])))
    }
  }

  @VisibleForTesting void handleLine(String line) {
    def eventType = eventType(line)

    switch (eventType) {
      case EventType.Call:
        handleCall(line)
        break
      case EventType.Fail:
      case EventType.Fix:
        handleFailOrFix(line, eventType)
        break
      case EventType.Stats:
      case EventType.Display:
        handleDisplayOrStats(line, eventType)
        break
      default:
        LOG.warn("Unrecognized scenario line: " + line)
        break
    }
  }

  /**
   * Theoretically we could inline this in {@link #handleLine(String)}, 
   * but this way it is easier to test this stuff separately
   */
  @VisibleForTesting static EventType eventType(String line) {
    if (line.startsWith("call")) {
      return EventType.Call;
    } else if (line.startsWith("fail")) {
      return EventType.Fail;
    } else if (line.startsWith("fix")) {
      return EventType.Fix;
    } else if (line.startsWith("display")) {
      return EventType.Display;
    } else if (line.startsWith("stats")) {
      return EventType.Stats;
    } else {
      throw new IllegalArgumentException("Unrecognized line format, cannot proceed: " + line)
    }
  }
}

